# 👉 手把手教你发个 npm 包

# 什么是 npm 包?

npm 是包管理工具,是前端模块化下的一个标志性产物。那什么是包呢?🍞 ?👜 ?❌ !node.js 中的第三方模块又叫做包,包是由第三方个人或团队开发出来的,供其他开发者使用的模块代码。通过 npm 这个包管理工具创建发布包和下载包,复用包里面的工具代码,从而提高工作效率。

本文将会通过实现一个版本比较的小功能,手把手教如何发一个 npm 包。

包含哪些内容?

  • 发布一个 Typescript 包,包含函数声明文件
  • rollup 打包,支持 COMMONJS / UMD / ES Module 格式
  • ESlint 校验, commit message 格式约束
  • 发包前自动升级版本并构建
  • 一些关于 npm 包的小 tips

# 准备

# 注册账号

发布包之前你必须拥有一个 npm 的账号,戳 https://www.npmjs.com/ 官网链接,注册一个账号。

# 创建项目

创建项目,初始化项目基本结构和更新 package.json 文件,用于描述当前项目的功能。

mkdir compare_version

cd compare_version

接着初始化 npm 包的一些配置:

npm init

// 接下过程是以问答式 CLI 方式进行
// Q1:package name: (文件夹名字) | 默认会以文件夹名字命名,当然也可以自定义包名,但需要遵守 npm 命名规范
// Q2:version: (1.0.0) | 版本号,默认是 1.0.0,具体版本号规则,可参考《语义化版本 2.0.0》
// Q3:description: | 包描述,用于描述这个包的主要功能以及用途
// Q4:entry point: (index.js) | 入口文件,即从那个文件开始执行
// Q5:test command: | 测试命令,用于包测试的命令,可以后续补充关于测试包的编写
// Q6:git repository: | 项目的 git 存储库地址
// Q7:keywords: | 描述包的关键字,用于在 npmjs 上查询关键字
// Q8:author: | 作者名字,可以使用 npmjs 名称、npmjs 注册邮箱、github 注册邮箱
// Q9:license: (ISC) | 开源协议,默认 ISC
// Q10:输出 package.js 内容并询问 Is this OK? (yes) 默认为 yes,回车后生成文件

在初始化的 package.json 文件中,它包含了你的项目信息以及众多配置项。除此之外,也可以写一个 README.md 文件用来描述你的项目。这里举个 package.json的具体配置例子:

{
    "name": "compare_version_lib",
    "version": "1.0.0",
    "description": "This is a js library that help you to compare two version number.",
    "main": "dist/index.umd.js",
    "module": "dist/index.esm.js",
    "types": "dist/index.d.ts",
    "scripts": {
        "dev": "rollup -w -c",
        "build": "rollup -c",
        "test": "jest",
        "prepare": "husky install"
    },
    "keywords": [],
    "author": "chieminchan",
    "license": "MIT",
    "repository": {
        "type": "git",
        "url": "https://github.com/chieminchan/compare_version_lib.git"
    },
    "files": ["src/", "dist/"],
    "devDependencies": {}
}
  • files 字段是用于约定在发包的时候 NPM 会发布包含的文件和文件夹。注意: files 字段中文件夹名直接写名字,不要包含 ./ 字符,否则打包出来的产物不会包含该文件夹。

# npm 包命名机制

在 npm 的包管理系统中,有一种 scoped packages 机制,用于将一些 packages 以 @scope/package 的命名形式,让同个域级的包集中在一个命名空间下面,实现域级的包管理,同时还能避免包名冲突的情况。

  • npm view packageName 查看包是否被占用,并可以查看它的一些基本信息,若包名称从未被使用过,则会抛出 404 错误。

  • 在初始化项目时,可以使用命令行来添加 scope:npm init --scope=scopeName

  • 同域级范围内的包会被安装在相同的文件路径下 node_modules/@scopeName/: 562 qQ5d1I.png

  • 例如:vue 框架里面涉及的一些脚手架工具包:@vue/cli-plugin-babel、@vue/cli-plugin-eslint 等等域级包。

# 初始化 Typescript 环境

我们打包给用户,在用户引用时,还需要支持在 TS 环境下的代码提示,所以再来快速初始化生成一个 tsconfig.json 文件,该文件属于 Typescript 配置文件。

npm add typescript -D

tsc --init

按需配置tsconfig.json文件:

{
    "compilerOptions": {
        "rootDir": "./src",
        "declaration": true,
        "declarationDir": "./dist",
        "outDir": "./dist",
        "target": "ES2015",
        "module": "ES2020",
        "strict": true,
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true
    }
}
  • 使用 declaration 会自动生成类型声明文件。配置后,在编译过程中会向 ./dist 目录输出 index.d.ts 类型声明文件
  • target 表示要编译出的代码格式,一般使用 es6
  • module 表示源代码的语法版本,这是使用最新的 es2020。

# 引入打包编译工具

包发布前,我们需要将编写的代码打包编译成 js 代码,并进行相关压缩操作。

rollup 是一个 JavaScript 模块打包器,在功能上要完成的事和 webpack 性质是一样的,就是将小块代码编译成大块复杂的代码。在平时开发应用程序时,我们基本上选择用 webpack。这里使用 rollup,相比 webpack 它更轻量,配置少,比较适合工具库的打包。

这里需要先初始化 Rollup 打包环境:

  • rollup 安装
npm install rollup -g        # 全局安装
npm install rollup -D        # 项目本地安装

npm install @rollup/plugin-typescript -D  # 将Typescript转换成为 ES6+ 标准

npm install @rollup/plugin-commonjs -D    # rollup默认不支持CommonJS,自己写的时候可以尽量避免使用CommonJS模块的语法,但有些外部库的是cjs或者umd(由webpack打包的)。如果使用这些外部库就需要支持CommonJS模块。

npm install @rollup/plugin-node-resolve -D
  • 创建配置文件 rollup.config.js
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";

export default {
    input: "src/index.ts",
    output: [
        {
            file: "dist/index.esm.js",
            format: "es",
        },
        {
            file: "dist/index.umd.js",
            format: "umd",
            name: "index.umd.js",
        },
    ],
    plugins: [commonjs(), typescript()],
};

上述文件表示我们要打包编译出两个文件,分别是 UMD 格式的和 ES Module 格式的。UMD 格式需要定义一个变量,这样在浏览器环境下,所有的方法都会挂载在这个变量上了。

  • 增加编译命令
{
    "script": {
        "dev": "rollup -w -c",
        "build": "rollup -c"
    }
}

如此,我们便可以通过 npm run dev,就能在开发的时候实时编译。如果需要打包,则先执行 npm run build 命令即可完成编译打包。

# ESlint 配置

运行初始化配置命令,根据项目情况来设置特地配置:

npm init @eslint/config

其中,会触发安装以下三个依赖:

npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
  • eslint:ESLint 的核心代码

  • @typescript-eslint/parser:ESLint 的解析器,用于解析 typescript。因为 eslint 检测代码的核心原理是,先将 JS 代码生成 AST,然后遍历 AST,在不同类型的节点进行不同规则的批评校验。而 TS 无法直接生成 AST,因此需要先 parser ,才能进一步对 Typescript 代码进行检查和规范

  • @typescript-eslint/eslint-plugin:这是一个 ESLint 插件,包含了各类定义好的检测 Typescript 代码的规范

最终,项目根目录下会生成.eslintrc.json文件,该文件中定义 ESLint 的基础配置,比如:一个简单的配置如下:

{
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "plugins": ["@typescript-eslint"],
    "rules": {}
}
  • eslint:recommended,这个配置文件的 rules 部分开启了所有 ESLint 推荐使用的规则
  • 同时,也可以通过 plugin 插件引入可共享配置。为plugin:{plugin_pacakge_name}/{config_name}
  • extends 允许配置多个模块,如果模块间的配置有冲突,位置靠后的配置会覆盖前面的。
  • 个性化的约束规则可以在 rules 进行设置,当 rules 和 extends 中配置了相同规则,rules 中的配置的优先级会高于 extends。

# Husky - Git 提交约束

Eslint 对我们编码格式进行了约束,但难以避免没有按要求进行格式化代码就提交到了远程的情况。为此,引入 Husky,在git commit 提交前进行自动格式化暂存区内文件,以及校验是否符合 Eslint 规则,从而明确统一远程仓库代码的规范。

# Husky 是什么?

在项目的 .git/hooks 文件夹内有很多 Hooks,这些钩子会在 git 执行的某些节点被触发。以一次 commit 为例,会先后触发 pre-commitprepare-commit-msgcommit-msgpost-commit 等 hooks。

随机打开其中的 .git/hooks/pre-commit.sample,可以看到,实质上是一个 shell 脚本。Git 提供了一个自定义 Hooks 文件夹的 config,可以通过设置 core.hooksPath指向自定义目录,即,我们可以自定义设置 shell 脚本来实现个性化功能。

回到主题上,Husky 就是能够简化创建或者修改 Githooks 过程的工具。

它最新的运作原理大概就是:

  1. husky install 安装时创建 hooks,将 git hooks 的目录指定为.husky/
  2. 提交时从配置文件中(package.json.huskyrc.huskyrc.json...)读取相应的 hook 配置,使用 husky add 命令向 .husky/ 中添加 hook
  3. 特定时机,触发执行配置中的指令/脚本

具体原理结合源码分析可以参考: Husky 原理解析及在代码 Lint 中的应用 (opens new window)

# Husky 配合 lint-staged 食用更佳

  • 引入 commitlint,校验提交的 message 格式是否符合规范,以此统一 git commit message。
  • 引入 lint-staged,针对当次提交(暂存区的代码)的 tsjs 文件进行检测。
npm install husky -D
npm install @commitlint/cli -D
npm install @commitlint/config-conventional -D

npm install lint-staged -D

husky 6.0.0 版本以前,安装 lint-staged 后,需要在 package.json 文件中,设置我们需要的 git hooks:

"husky": {
  "hooks": {
    // 在 commit 之前先,针对lint-staged配置的文件执行配置好的指令
    "pre-commit": "lint-staged",
    // 校验 commit 时添加的备注信息是否符合要求规范
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  }
},
"commitlint": {
  "extends": ["@commitlint/config-conventional"]
},
"lint-staged": {
  "*.{ts,js}": [
    "node --max_old_space_size=8192 ./node_modules/.bin/prettier -w",
    "node --max_old_space_size=8192 ./node_modules/.bin/eslint --fix --color",
    "git add"
  ]
}

以上的配置表示开发者在 git commit 时,会首先调用 lint-staged 相关命令: prettier 格式化,然后是 ESlint 校验并修复,然后将修改后的文件存入暂存区,然后是校验 commit message 是否符合规范,符合规范后才会成功 commit。

husky 6.0.0 版本后,不再使用 .huskyrc.js 文件,同时也不支持在 package.json 文件中进行 husky 相关配置,而是在 .husky/ 目录中配置的单个 git 钩子。

可以直接使用 npx mrm@2 lint-staged 代替 npm install lint-staged -D,它会直接在项目中配置好 husky 和 lint-staged,你只需要在 package.json 中修改一下 lint-staged 检测文件范围

% npx mrm@2 lint-staged
npx: 237 安装成功,用时 21.067 秒
Running lint-staged...
Update package.json
husky - Git hooks installed
husky - created .husky/pre-commit
"lint-staged": {
  "{*.js,jsx,ts,tsx}": "eslint --cache --fix"
},

# 开发

到这一步,可以开始编写代码啦!

  • 项目基本配置文件外,npm module 里的一般包括三个文件夹:dist | src | types

    • dist 包编译后最终产出文件
    • src 源码文件
    • types 类型声明文件
  • 新建 src/index.ts 文件

/*
 * compare version
 * 比较版本号,版本号规则x.y.z,xyz均为大于等于0的整数
 * 0: 相等
 * 1: 大于
 * -1: 小于
 */
/*
 * compare version
 * 比较版本号,版本号规则x.y.z,xyz均为大于等于0的整数
 * 0: 相等
 * 1: 大于
 * -1: 小于
 */

const COMPARE_RESULT_MAP = {
    BIGGER: 1,
    SMALLER: -1,
    SAME: 0,
};

function versionCompare(version1: string, version2: string): number {
    const version1Arr = version1.split(".");
    const version2Arr = version2.split(".");
    const maxLength = Math.max(version1Arr.length, version2Arr.length);
    for (let i = 0; i < maxLength; i++) {
        const num1 = +version1Arr[i] || 0;
        const num2 = +version2Arr[i] || 0;

        if (num1 !== num2) {
            return num1 > num2
                ? COMPARE_RESULT_MAP.BIGGER
                : COMPARE_RESULT_MAP.SMALLER;
        }
    }

    return COMPARE_RESULT_MAP.SAME;
}

export default versionCompare;

# 发布

# 设置发布仓库 registry

在下载包时,有些人会偏向于设置 taobao 镜像,因为 npm 仓库服务器在国外,下载速度比较慢。发布的时候也一样,一般开源应用基本都发布到 npmjs,公司内部包的话就会发到私有 npm 仓库,我们可以在 package.json 设置一下你想要发布的仓库地址,默认是执行 publish 命令所指向的仓库:

"publishConfig": {
    "registry": "http://registry.npm.xxx.com/"
 }

也可以设置别名

// 设置别名
alias ynpm="npm --registry=http://registry.npm.xxx.com"

// 发布
ynpm publish

发布包需要验证你的账号权限,第一次执行需要 npm login

# 版本管理

npm 的发包需要遵循语义化版本,一个版本号包含三个部分:major.minor.patch

我们可以使用 npm version 命令来自动修改版本号,比如:

// version = v1.0.0

npm version major // 主版本号:当你做了不兼容的API修改
# v2.0.0

npm version minor // 次版本号:当你做了向下兼容的功能性新增,可以理解为Feature版本
# v2.1.0

npm version patch // 修订号:当你做了向下兼容的问题修正,可以理解为Bug fix版本
# v2.1.1

一般来说还有先行版本,测试版本等,先行版本号是加到修订号后面,作为版本号的延伸;版本中携带 alphabetarc 等 tag 字样,统称先行版,带有预发布版本号的,一般格式为 x.y.z-[tag].[次数 / meta 信息]。比如:3.1.0-beta.03.1.0-alpha.0

当要发行大版本或核心功能时,但不能保证这个版本完全正常,就要先发一个先行版本。我们可以使用 --preid 参数来发布先行版本,: npm version prerelease --preid=alpha

npm version premajor // 预备主版本
# v3.0.0-0

npm version preminor // 预备次版本
# v3.1.0-0

npm version prepatch // 预备修订版本
# v3.1.1-0

npm version prerelease // 预发布版本
# v3.1.1-1

npm version prerelease --preid=alpha // 命名先行版,测试版用beta
# v3.1.1-alpha.0

npm version prerelease // 升级先行版
# v3.1.1-alpha.1

运行 npm version 时,本地 git status 需要是 clear 的,因为运行命令之后会自动提交一个 commit 并打上 tag,也可以自定义 commit message。

npm version patch -m "升级到 %s: 演示自定义msg"
# v3.1.1

# Changelog

包发布了很多次后,使用者升级就需要知道他是否需要升级,需要查看文档看看有哪些不兼容性改动,所以需要一个 Changelog 来记录每次发布改了些什么。

如果手动的维护肯定会有忘记的时候,所以需要使用工具来自动生成,我们可以使用 standard-version 这个包来生成,这个包的作用是自动更新版本和生成 CHANGELOG。

npm install --D standard-version # 安装 standard-version

将 npm run 脚本添加到您的 package.json:

{
    "scripts": {
        "patch": "standard-version --patch",
        "release": "standard-version",
        "release:alpha": "standard-version --prerelease alpha"
    }
}

现在就可以使用 npm run release --patch 代替 npm version

% npm run patch

> compare_version_lib@1.1.1 patch /compare_version
> standard-version --patch

✔ bumping version in package.json from 1.1.1 to 1.1.2
✔ bumping version in package-lock.json from 1.1.1 to 1.1.2
✔ outputting changes to CHANGELOG.md
✔ committing package-lock.json and package.json and CHANGELOG.md
→ No staged files match any configured task.
✔ tagging release v1.1.2
ℹ Run `git push --follow-tags origin master && npm publish` to publish

// 会根据每个commit生成Changelog:

# Changelog

### [1.1.2](https://github.com/chieminchan/compare_version_lib/compare/v1.1.1...v1.1.2) (2022-03-21)

### Features
-   update srcript ([2ea0d93](https://github.com/chieminchan/compare_version_lib/commit/2ea0d9369ed6c1019bc99a5fc1b2685727def39c))

# TIPS:

# package.json 的依赖版本号

在 package.json 的一些依赖的版本号中,我们还会看到^、~或者>=这样的标识符,或者不带标识符的,这都代表什么意思呢?

没有任何符号:完全百分百匹配,必须使用当前版本号对比符号类的:>(大于) >=(大于等于) <(小于) <=(小于等于) 波浪符号~:固定主版本号和次版本号,修订号可以随意更改,例如~2.0.0,可以使用 2.0.0、2.0.2 、2.0.9 的版本。插入符号^:固定主版本号,次版本号和修订号可以随意更改,例如^2.0.0,可以使用 2.0.1、2.2.2 、2.9.9 的版本。任意版本*:对版本没有限制,一般不用或符号:||可以用来设置多个版本号限制规则,例如 >= 3.0.0 || <= 1.0.0

  • 通过 jsdelivr 可以方便的查看包内容
  • np 包,进行不同源的 npm 包发布管理的

参考文章:

  • https://juejin.cn/post/6844903870678695943#heading-0
  • https://zhuanlan.zhihu.com/p/366786798